#define _GLIBCXX_USE_NANOSLEEP //required for this_thread::sleep_for
#include <iostream>
#include "SDL.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <cmath>
#include <chrono>
#include <thread>
#include "mesh.h"
#include "mesh_util.h"
#include "transform.h"
#include "surface_line.h"
#include "util.h"
#include "screenshot.h"
#include <omp.h>
using namespace std;
float t = 0.0;

struct settings {
    bool smooth_normals;
    int mesh_id;
    int subdiv_level;
    float view_y;
    float view_x;
    float mouse_x;
    float mouse_xo;
    float mouse_y;

    float look_angle;
    vec2f look_uv;
    int look_face;
    bool forward;
};

const int w = 600;
const int h = 400;
const int bw = 64;
const int bh = 64;
const float shadingrate = 1.0;
const int gridsize = (float(bw * bh) / shadingrate) * 1.0f;
int frame_n = 0;

settings _globals = {false, 0, 5, 0, 0, 0, 0, 0, M_PI / 3.0, {0.5, 0.5}, 0};

float frand() { return float(rand()) / float(RAND_MAX); }
float hrand() { return frand() * 2.0 - 1.0; }

void draw(mesh& surface, const vector<PtexTexture*>& textures, const vector<PtexFilter*>& filters) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

        gluLookAt(0, 2.0, 0.0, 0.0, sin(_globals.mouse_y / 400.0f - .5f) * 5.0, cos(_globals.mouse_y / 400.0f - 1.0f) * 5.0, 0.0, 1.0, 0.0);
    //gluLookAt(0.0, 0, 2.0, 0, 0, 0, 0, 1, 0);

    mesh& m2 = surface;
    //for (int i = 0; i < _globals.subdiv_level; ++i) m2 = m2.subdivide();
    //m2.render(t, _globals.smooth_normals);

    surface_line forward(m2, _globals.look_face, fmod(_globals.look_angle + (_globals.view_x) / 200.0f + M_PI * 20.0, 2.0 * M_PI), _globals.look_uv[0], _globals.look_uv[1], 20.0f, 4000);
    
    float movef = 0.0f;
    if (_globals.forward) movef = 0.005f;
    uv pos = forward.uv_at(movef);

    //surface_line forward(m2, 0, M_PI / 3.0, 0.0, 0.0, 20.0f, 4000);
    //uv pos = forward.uv_at(fmod(frame_n / 50.0f, 100.0f) + 0.001f);
    _globals.look_face = pos.face * 4.0;
    _globals.look_uv = pos.uv;
    _globals.look_angle = pos.ang;

    int num_lines = 100;
    int num_samples = 100;
    float fov = M_PI / 4.0f;
    float hfov = fov / 2.0f;

    vector<surface_line*> lines(num_lines);

    #pragma omp parallel for schedule(dynamic, 10)
    for (int i = 0; i < num_lines; ++i) {
        float angle_part = float(i) / num_lines * fov - hfov;
        lines[i] = new surface_line(m2, _globals.look_face, _globals.look_angle + angle_part, _globals.look_uv[0], _globals.look_uv[1], 2.0f, 400);
    }

    grid<pair<vertex,vertex>> G(num_lines, num_samples);

    #pragma omp parallel for schedule(dynamic, 10)
    for (int i = 0; i < num_lines; ++i) {
        int T = omp_get_thread_num();
        for (int j = 0; j < num_samples; ++j) {
            float base_x = sin(float(i) / num_lines * fov - hfov) * 10.0f;
            float base_y = cos(float(i) / num_lines * fov - hfov) * 10.0f;
            float d = float(j) / float(num_samples);
            d = pow(d, 4.0f) + 0.1f;

            vertex a = d * vertex{base_x, 0.0f, base_y};
            uv coords = lines[i]->uv_at(0.0001f + d);
            vec2f st = m2.get_st(coords.face, coords.uv);
            float data[4];
            int orig_face = coords.face / pow(4, _globals.subdiv_level);

            if (isnan(st[0])) st[0] = 0.5f;
            if (isnan(st[1])) st[1] = 0.5f;

            float min_u = textures[T]->getFaceInfo(orig_face).res.u();
            float min_v = textures[T]->getFaceInfo(orig_face).res.v();
            min_u = 2.0f / min_u;
            min_v = 2.0f / min_v;

            float fw = (d - 0.1f) / 12.0f;
            if (fw < min_u) fw = min_u;
            if (fw < min_v) fw = min_v;
            filters[T]->eval(data, 0, 4, orig_face, st[1], st[0], 1.0, 0.0, 0.0, 1.0, fw, 0.05f);

            a[1] = (data[0] + data[1] + data[2]) * 0.2f - 0.2f;
            G(i,j) = make_pair(a, vertex{data[0], data[1], data[2]});
        }
    }

    for (int i = 0; i < num_lines -1; ++i) {
        for (int j = 0; j< num_samples -1; ++j) {
            //int ii = (i + 1) % num_lines; //full sweep
            int ii = i + 1;
            glBegin(GL_QUADS);
            
            glColor3f(G(i,j).second);
            glVertex3f(G(i,j).first);
            
            glColor3f(G(ii,j).second);
            glVertex3f(G(ii,j).first);
            
            glColor3f(G(ii,j+1).second);
            glVertex3f(G(ii,j+1).first);
            
            glColor3f(G(i,j+1).second);
            glVertex3f(G(i,j+1).first);
            
            glEnd();
        }
    }

    for_each(lines.begin(), lines.end(), [](surface_line* l) { delete l; });

    t += 0.01;
    frame_n++;

    SDL_GL_SwapBuffers(); 
    srand(0);
}

bool do_stuff() {
    //reset view rotation
    _globals.view_x = 0;
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        switch (event.type) {
            case SDL_QUIT: 
                return false;
                break;
            case SDL_KEYDOWN:
                switch( event.key.keysym.sym ) {
                    case SDLK_u:
                        _globals.subdiv_level++;
                        break;
                    case SDLK_i:
                        _globals.subdiv_level--;
                        break;
                    case SDLK_n:
                        _globals.smooth_normals = !_globals.smooth_normals;
                        break;
                    case SDLK_w:
                        _globals.forward = true;
                        break;
/*                    case SDLK_1:
                        _globals.mesh_id = 0;
                        break;
                    case SDLK_2:
                        _globals.mesh_id = 1;
                        break;
                    case SDLK_3:
                        _globals.mesh_id = 2;
                        break;
                    case SDLK_4:
                        _globals.mesh_id = 3;
                        break;
                    case SDLK_5:
                        _globals.mesh_id = 4;
                        break;
                    case SDLK_6:
                        _globals.mesh_id = 5;
                        break;
                    case SDLK_7:
                        _globals.mesh_id = 6;
                        break;
                    case SDLK_8:
                        _globals.mesh_id = 7;
                        break;
                    case SDLK_9:
                        _globals.mesh_id = 8;
                        break;*/
                    case SDLK_UP:
                        _globals.view_y += 0.1f;
                        break;
                    case SDLK_DOWN:
                        _globals.view_y -= 0.1f;
                        break;
                    case SDLK_LEFT:
                        _globals.view_x -= 0.1f;
                        break;
                    case SDLK_RIGHT:
                        _globals.view_x += 0.1f;
                        break;
                    default:
                        break;
                }
                break;
            case SDL_KEYUP:
                switch (event.key.keysym.sym) {
                    case SDLK_w:
                    _globals.forward = false;
                    break;
                    default:
                    break;
                }
                break;
            case SDL_MOUSEMOTION:
                _globals.mouse_x = event.motion.x;
                _globals.mouse_y = event.motion.y;
                _globals.view_x = event.motion.xrel; 
                _globals.view_y = event.motion.yrel;
                if (abs(_globals.view_x) > 100) _globals.view_x = 0;
                break;
            default:
                break;
        }

    }
    return true;
}


int main() {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_SetVideoMode(w, h, 32, SDL_OPENGL);

    //glShadeModel(GL_FLAT);
    glBlendFunc(GL_SRC_ALPHA,GL_ONE);
    glEnable(GL_DEPTH_TEST);
    //glEnable(GL_BLEND);
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glClearDepth(1.0);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(30.0, float(w)/float(h), 0.001, 100.0);
    //glOrtho(0, w, 0, h, 0.01, 100.0);

    /*vector<mesh> surfaces;
    surfaces.push_back(cube());
    surfaces.push_back(tetra());
    surfaces.push_back(mesh_from_obj("models/tube.obj"));
    surfaces.push_back(mesh_from_obj("models/torus.obj"));
    surfaces.push_back(mesh_from_obj("models/tetrashape.obj"));
    surfaces.push_back(mesh_from_obj("models/bucket.obj"));
    surfaces.push_back(mesh_from_obj("models/squirrel.obj"));
    surfaces.push_back(mesh_from_obj("models/flat.obj"));
    surfaces.push_back(mesh_from_obj("models/twisted.obj"));*/

    auto ftime = chrono::microseconds(1000000 / 60);
    auto start = chrono::high_resolution_clock::now();
    auto last = start;

    mesh m = mesh_from_obj("models/twisted.obj");
    //mesh m = cube();
    m.prepare();

     //m.write_ptex(400.0f);

    std::string error;
    vector<PtexTexture*> textures;
    for (int i = 0; i < omp_get_max_threads(); ++i) {
        textures.push_back(PtexTexture::open("foo.ptx", error));
        //TODO check we got a texture
    }

    vector<PtexFilter*> filters;
    for (int i = 0; i < omp_get_max_threads(); ++i) filters.push_back(PtexFilter::getFilter(textures[i], PtexFilter::Options(PtexFilter::f_bspline, true)));

    for (int i = 0; i < _globals.subdiv_level; ++i) m = m.subdivide();

    while (do_stuff()) {
        start = chrono::high_resolution_clock::now();
        //draw(surfaces.at(_globals.mesh_id));
        draw(m, textures, filters);

        //screenshot(w, h, "flat", frame_n);

        auto d = start - last;
        last = start;
        if (d < ftime) {
            this_thread::sleep_for(ftime - d);
            last = start + ftime - d;
        } else {
            //cout << 1.0 / chrono::duration<double>(chrono::high_resolution_clock::now() - start).count() << endl;
        }            
    }
    SDL_Quit();

    for(size_t i = 0; i < filters.size(); ++i) {
        filters[i]->release();
        textures[i]->release();
    }

    return 0;
}
